package com.github.tommas.admintpl.security;

import com.github.tommas.admintpl.bean.model.User;
import com.github.tommas.admintpl.bean.vo.AuthorizationInfo;
import com.github.tommas.admintpl.security.exception.AccountNotFoundException;
import com.github.tommas.admintpl.security.permission.RestrictedPermission;
import com.github.tommas.admintpl.security.permission.RestrictedPermissionResolver;
import com.github.tommas.admintpl.service.AccountService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Hash;
import org.apache.shiro.lang.util.SimpleByteSource;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.subject.WebSubject;

import javax.servlet.ServletRequest;
import java.util.HashSet;
import java.util.List;


public class AccountRealm extends AuthorizingRealm {
    public static final String REALM_NAME = AccountRealm.class.getName();

    public static final String LOGIN_USER_ATTR = AccountRealm.class.getName() + ":login_user";
    public static final String AUTHORIZATION_INFO_ATTR = AccountRealm.class.getName() + ":authorization_info";
    public static final String AUTHORIZED_PERMISSION_KEY = AccountRealm.class.getName() + ":authorized_permission";

    private AccountService accountService;

    public AccountRealm(AccountService service) {
        accountService = service;
        setCredentialsMatcher(new HashedCredentialsMatcher(service.getPasswordHashAlgorithm()));
        setPermissionResolver(new RestrictedPermissionResolver());
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken authenticationToken) throws AuthenticationException {
        AuthenticationToken token = (AuthenticationToken) authenticationToken;

        Hash hashedPass = accountService.getHashedPassword(String.valueOf(token.getPassword()), token.getUsername());
        User user = accountService.login(token.getUsername(), hashedPass.toHex());
        if (user == null) {
            throw new AccountNotFoundException(String.format("Cannot find account for user: %s", token.getUsername()));
        }

        SimplePrincipalCollection pc = new SimplePrincipalCollection();
        pc.add(user.getUsername(), REALM_NAME);
        pc.add(user.getId(), REALM_NAME);

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(pc, hashedPass);
        authenticationInfo.setCredentialsSalt(new SimpleByteSource(token.getUsername()));

        WebSubject subject = (WebSubject) SecurityUtils.getSubject();
        subject.getServletRequest().setAttribute(LOGIN_USER_ATTR, user);

        return authenticationInfo;
    }

    @Override
    protected SimpleAuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        AuthorizationInfo authorizationInfo = accountService.getUserAuthorizationInfo(
                UserPrincipal.fromPrincipalCollection(principalCollection));

        WebSubject subject = (WebSubject) SecurityUtils.getSubject();
        ServletRequest request = subject.getServletRequest();
        SimpleAuthorizationInfo shiroInfo;
        if ((shiroInfo = (SimpleAuthorizationInfo) request.getAttribute(AUTHORIZATION_INFO_ATTR)) != null) {
            return shiroInfo;
        }

        shiroInfo = new SimpleAuthorizationInfo();
        if (authorizationInfo == null) {
            return shiroInfo;
        }

        List<RestrictedPermission> permissions;
        if ((permissions = authorizationInfo.getPermissions()) != null) {
            shiroInfo.setObjectPermissions(new HashSet<>(permissions));
        }

        // cache at request level
        request.setAttribute(AUTHORIZATION_INFO_ATTR, shiroInfo);

        return shiroInfo;
    }

    @Override
    protected boolean isPermitted(Permission permission, org.apache.shiro.authz.AuthorizationInfo info) {
        for (Permission perm : info.getObjectPermissions()) {
            if (permission.implies(perm)) {
                // cache authorized permission for this request
                ThreadContext.put(AUTHORIZED_PERMISSION_KEY, perm);
                return true;
            }
        }

        return false;
    }
}